package jbeagle.concurrent.server;

import java.rmi.NoSuchObjectException;
import java.rmi.NotBoundException;
import java.rmi.RemoteException;
import java.rmi.registry.LocateRegistry;
import java.rmi.registry.Registry;
import java.rmi.server.UnicastRemoteObject;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import java.util.Observable;
import java.util.Observer;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.TimeUnit;

import org.apache.log4j.Logger;

import jbeagle.concurrent.remote.ConcurrentEvaluationManager;
import jbeagle.core.FitnessFunction;
import jbeagle.core.Individual;
import jbeagle.core.Population;


public class DefaultEvaluationManager<I extends Individual<G>, G> implements ConcurrentEvaluationManager,
																				FitnessFunction<I, G> {
	
	static Logger log = Logger.getLogger(DefaultEvaluationManager.class);
	
	protected BlockingQueue<I> evaluationQueue,
								completedQueue;
	
	private long completionGiveUpTime;
	
	private boolean moreToEvaluate;
	private String remoteName;
	
	private Class<? extends FitnessFunction> fitnessClass;
	
	public DefaultEvaluationManager() {
		createQueues();
		moreToEvaluate = true;
		setCompletionGiveUpTime(30000);
	}
	
	protected void createQueues() {
		evaluationQueue = new LinkedBlockingQueue<I>();
		completedQueue = new LinkedBlockingQueue<I>();
	}
	
	public boolean addIndividual(I ind) {
		if ( ind == null )
			throw new NullPointerException("Cannot add an individual that is null to the queue.");
		try {
			evaluationQueue.put(ind);
		}
		catch ( InterruptedException ie ) {
			return false;
		}
		return true;
	}

	public I getNextToEvaluate() {
		try {
			return evaluationQueue.take();
		} catch ( InterruptedException ie ) {}
		return null;
	}
	
	/**
	 * {@inheritDoc}
	 */
	public I pollNextToEvaluate(long timeout, TimeUnit unit) {
		try {
			return evaluationQueue.poll(timeout, unit);
		} catch ( InterruptedException ie ) {}
		return null;
	}
	
	public void setEvaluationsDone(boolean done) {
		moreToEvaluate = !done;
	}
	
	/**
	 * {@inheritDoc}
	 */
	public boolean hasMoreToEvaluate() {
		return moreToEvaluate;
	}
	
	public int waitingIndividuals() {
		return evaluationQueue.size();
	}
	
	/**
	 * {@inheritDoc}
	 */
	public void setEvaluationComplete(Individual ind) {
		try {
			completedQueue.put((I)ind);
		} catch ( InterruptedException ie ) {}
	}
	
	public I getNextCompletedIndividual() {
		try {
			return completedQueue.take();
		} catch ( InterruptedException ie ) {
			log.warn("Interrupted before all tasks could be processed.");
		}
		return null;
	}
	
	public I pollNextCompletedIndividual(long timeout, TimeUnit unit) {
		try {
			return completedQueue.poll(timeout, unit);
		} catch ( InterruptedException ie ) {
			log.warn("Interrupted before all tasks could be processed.");
		}
		return null;
	}
	
	public long getCompletionGiveUpTime() {
		return completionGiveUpTime;
	}

	/**
	 * Set a timeout parameter in milliseconds that indicates how long the manager should wait before
	 * giving up on Individuals that haven't been completed. This tries to make the genetic algorithm
	 * continue even though an Evaluator thread or process has failed for some reason (and thus the
	 * Individual taken from the queue by that Evaluator will never be completed).
	 * 
	 * The manager will force continuation of the GA if there are no more Individuals waiting to be
	 * evaluated, but not all Individuals have been completed and a time equal to completionGiveUpTime
	 * has passed since the last Individual was taken from the queue of Individuals to be evaluated.
	 * 
	 * @param completionGiveUpTime
	 */
	public void setCompletionGiveUpTime(long completionGiveUpTime) {
		this.completionGiveUpTime = completionGiveUpTime;
	}
	
	///// FITNESS FUNCTION METHODS /////
	
	@Override
	public void evaluate(I ind) {
		addIndividual(ind);		
	}

	@Override
	public void evaluate(Population<I, G> pop) {
		log.debug("Starting evaluation of population of size " + pop.size());
		//if an Evaluator made a "late submission" in the previous evaluation, clear it
		completedQueue.clear(); 
		
		for ( I ind : pop )
			addIndividual(ind);
		
		List<I> completed = new ArrayList<I>(pop.size());

		while ( completed.size() < pop.size() ) {
			I ind = pollNextCompletedIndividual(completionGiveUpTime, TimeUnit.MILLISECONDS);
			if ( ind != null ) {
				completed.add(ind);
			}
			else {
				log.debug("There are currently " + waitingIndividuals() + " individuals waiting to be evaluated.");
				log.debug("Waiting for " + (pop.size() - completed.size()) + " individuals to be completed.");
				if ( waitingIndividuals() == 0 ) {
					log.info("No more individuals waiting to be evaluated, and waited more than " + completionGiveUpTime + "ms for completion. Forcing continuation.");
					forceContinuation(pop, completed);
					break;
				}
			}
		}
		
		pop.clear();
		pop.addAll(completed);
		
		log.debug("Population evaluation done.");
	}
	
	/**
	 * Forces continuation of GA if no more Individuals are waiting to be evaluated, but not all have
	 * been completed, and a time greater than completionGiveUpTime has passed since the last
	 * Individual was taken from the queue for evaluation.
	 * 
	 * The method calculates the average fitness of the Individuals that have been completed, then
	 * sets the fitness of the Individuals that have not been completed to that average fitness.
	 * 
	 * @param pop the original Population submitted for evaluation
	 * @param completed the list of Individuals that have been completed so far
	 */
	protected void forceContinuation(Population<I, G> pop, List<I> completed) {
		List<I> missing = new ArrayList<I>(pop.size() - completed.size());
		double avgFitness = 0;
		int index;
		for ( I ind : pop ) {
			index = completed.indexOf(ind);
			if ( index == -1 )
				missing.add(ind);
			else
				avgFitness += completed.get(index).getFitness();
		}
		avgFitness /= completed.size();
		
		for ( I ind : missing ) {
			ind.setFitness(avgFitness);
			completed.add(ind);
		}
		
		assert ( completed.size() == pop.size() );
	}
	
	@Override
	public Class<? extends FitnessFunction> getFitnessFunctionDef() {
		return fitnessClass;
	}
	
	public void setFitnessFunctionDef(Class<? extends FitnessFunction> fitnessClass) {
		this.fitnessClass = fitnessClass;
	}
	
	
	///// RMI METHODS /////
	public boolean registerAsRMIServer(String remoteName) {
		return registerAsRMIServer(remoteName, 0);
	}
	
	public boolean registerAsRMIServer(String remoteName, int port) {
		this.remoteName = remoteName;
		
		if (System.getSecurityManager() == null) {
            System.setSecurityManager(new SecurityManager());
        }
        try {
        	ConcurrentEvaluationManager stub =
                (ConcurrentEvaluationManager) UnicastRemoteObject.exportObject(this, port);
            Registry registry = LocateRegistry.getRegistry();
            registry.rebind(remoteName, stub);
           log.info(remoteName + " bound.");
        } catch (Exception e) {
            log.error("Exception occurred while attempting RMI registration:\n" + e);
            return false;
        }
        return true;
	}
	
	public boolean deregisterAsRMIServer() {
		if ( remoteName == null ) {
			log.warn("Never registered as RMI server, so deregistration is not necessary.");
			return true;
		}
		
		try {
			Registry registry = LocateRegistry.getRegistry();
			registry.unbind(remoteName);
		} catch (RemoteException re) {
            log.error("Remote exception while attempting to deregister:\n" + re);
            return false;
        } catch (NotBoundException be) {
        	log.warn(remoteName + " wasn't bound.");
        }
        
        boolean unexported = false;
        try {
        	unexported = UnicastRemoteObject.unexportObject(this, false);
        } catch (NoSuchObjectException e) {
        	return true;
        }
        return unexported;
	}
}
